package com.ruoyi.common.utils;

import com.ruoyi.common.core.annotation.DiffLog;
import lombok.Data;
import org.springframework.util.CollectionUtils;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;

public class ObjectDiffUtil<T> {
    private final OnDiffSuccess<T> onDiffSuccess;

    private ObjectDiffUtil(OnDiffSuccess<T> onDiffSuccess) {
        this.onDiffSuccess = onDiffSuccess;
    }

    public static <T> ObjectDiffUtil<T> onDiffSuccess(OnDiffSuccess<T> onDiffSuccess) {
        return new ObjectDiffUtil<>(onDiffSuccess);
    }

    public interface OnDiffSuccess<T> {
        /**
         * 生成对比记录
         *
         * @param key           对比的key
         * @param keyDesc       对比的key描述
         * @param obj           对比的对象
         * @param oldObj        对比的旧对象或属性值
         * @param newObj        对比的新对象或属性值
         * @param diffType      对比类型
         * @param fieldTypeEnum 对比字段类型
         * @param isJson
         * @return
         */
        T buildDiffLog(String key, String keyDesc, Object obj, Object oldObj, Object newObj, DiffType diffType, FieldTypeEnum fieldTypeEnum, boolean isJson);
    }

    public enum FieldTypeEnum {
        FIELD, OBJECT;
    }

    public enum DiffType {
        ADD, UPDATE, DELETE;
    }

    public List<T> diff(Object sourceObject, Object targetObject) throws Exception {
        return diff("", "", sourceObject, targetObject);
    }

    private List<T> diff(String key, String keyDesc, Object sourceObject, Object targetObject)
            throws Exception {
        List<T> diffLogRecordList = new ArrayList<>();

        if (sourceObject == null && targetObject == null) {
            return null;
        }

        if (sourceObject == null || targetObject == null) {
            Object obj = sourceObject == null ? targetObject : sourceObject;
            T diffLog = getDiffLog(key, keyDesc, obj, sourceObject, targetObject, FieldTypeEnum.OBJECT, false);
            if (diffLog != null) {
                diffLogRecordList.add(diffLog);
            }
            return diffLogRecordList;
        }

        if (!sourceObject.getClass().getName().equals(targetObject.getClass().getName())) {
            return null;
        }

        if (sourceObject.hashCode() == targetObject.hashCode()) {
            return null;
        }

        Field[] fields = sourceObject.getClass().getDeclaredFields();

        for (final Field field : fields) {
            Class<?> type = field.getType();
            String keyPath = field.getName();
            String fName;
            DiffLog logVo;
            field.setAccessible(true);
            if (field.isAnnotationPresent(DiffLog.class)) {
                logVo = field.getAnnotation(DiffLog.class);
                fName = logVo.name();
                if (logVo.ignore()) {
                    continue;
                }
            } else {
                continue;
            }
            if (Collection.class.isAssignableFrom(type)) {
                Class<?> GenericClass = null;
                if (field.getGenericType() instanceof ParameterizedType) {
                    GenericClass = (Class<?>) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0];
                }
                if (GenericClass == null) {
                    continue;
                }
                List<Field> keyFields = getCollKeyFieldList(GenericClass.getDeclaredFields());
                if (keyFields.isEmpty()) {
                    continue;
                }

                List<?> oldList = (List) field.get(sourceObject);
                List<?> newList = (List) field.get(targetObject);
                Map<Object, Object> oldFilterMap = new HashMap<>();
                Map<Object, Object> newFilterMap = new HashMap<>();
                String cKeyName = keyFields.stream()
                        .map(f -> f.getAnnotation(DiffLog.class).name())
                        .collect(Collectors.joining(","));

                if (newList != null) {
                    for (Object o : newList) {
                        newFilterMap.put(mergeKey(keyFields, o), o);
                    }
                }
                if (oldList != null) {
                    for (Object o : oldList) {
                        oldFilterMap.put(mergeKey(keyFields, o), o);
                    }
                }
                Set<Object> resultSet = new HashSet<>();
                resultSet.addAll(oldFilterMap.keySet());
                resultSet.addAll(newFilterMap.keySet());
                resultSet.remove(null);

                for (Object result : resultSet) {
                    Object oldOb = oldFilterMap.get(result);
                    Object newOb = newFilterMap.get(result);
                    List<T> collectDiff = diff(keyPath, cKeyName, oldOb, newOb);
                    if (!CollectionUtils.isEmpty(collectDiff)) {
                        diffLogRecordList.addAll(collectDiff);
                    }
                }
            } else {
                if (logVo.nested()) {
                    List<T> collectDiff = diff(keyPath, fName, field.get(sourceObject), field.get(targetObject));
                    if (!CollectionUtils.isEmpty(collectDiff)) {
                        diffLogRecordList.addAll(collectDiff);
                    }
                } else {
                    T diffLog = generateOneDiffs(keyPath, fName, field, sourceObject, targetObject);
                    if (diffLog != null) {
                        diffLogRecordList.add(diffLog);
                    }
                }

            }
        }
        return diffLogRecordList;

    }

    private List<Field> getCollKeyFieldList(Field[] collFields) {
        List<Field> keyFields = new ArrayList<>();
        for (Field collField : collFields) {
            if (collField.isAnnotationPresent(DiffLog.class)) {
                DiffLog keyFieldAnnotation = collField.getAnnotation(DiffLog.class);
                if (keyFieldAnnotation.key()) {
                    keyFields.add(collField);
                }
            }
        }
        return keyFields;
    }

    private String mergeKey(List<Field> keyFields, Object object) throws IllegalAccessException {
        keyFields.sort(Comparator.comparing(Field::getName));
        StringBuilder sb = new StringBuilder();
        for (Field keyField : keyFields) {
            keyField.setAccessible(true);
            sb.append(Optional.ofNullable(keyField.get(object)).map(String::valueOf).orElse(""));
        }
        return sb.toString();
    }

    private T generateOneDiffs(String key, String name, Field field, Object source, Object target) throws Exception {
        field.setAccessible(true);
        DiffLog logVo = field.getAnnotation(DiffLog.class);
        if (logVo == null) {
            return null;
        }
        Object oldValue = field.get(source), newValue = field.get(target);

        if (oldValue == newValue && oldValue == null) {
            return null;
        }

        Class<?> fieldType = field.getType();
        if (BigDecimal.class == fieldType && logVo.scale() != -1) {
            oldValue = oldValue == null ? null : ((BigDecimal) oldValue).setScale(logVo.scale(), logVo.roundingMode());
            newValue = newValue == null ? null : ((BigDecimal) newValue).setScale(logVo.scale(), logVo.roundingMode());
        }

        if (logVo.format().length > 0) {
            oldValue = formatObject(oldValue, logVo.format());
            newValue = formatObject(newValue, logVo.format());
        }

        if (logVo.ignoreLog()) {
            oldValue = null;
            newValue = null;
        }

        if (oldValue == null || newValue == null) {
            return getDiffLog(key, name, source, oldValue, newValue, FieldTypeEnum.FIELD, logVo.isJson());
        }
        if (!newValue.equals(oldValue)) {
            return getDiffLog(key, name, source, oldValue, newValue, FieldTypeEnum.FIELD, logVo.isJson());
        }
        return null;
    }

    private Object formatObject(Object obj, String[] format) {
        if (obj == null) {
            return null;
        }
        if (StringUtils.isEmpty(format)) {
            return obj;
        }
        String objStr = obj.toString();
        for (String rule : format) {
            String[] ruleArr = rule.split("=");
            if (ruleArr.length != 2) {
                continue;
            }
            if (objStr.equals(ruleArr[0])) {
                return StringUtils.replace(objStr, ruleArr[0],
                        String.format("%s (%s)", ruleArr[1], ruleArr[0]));
            }
        }
        return obj;
    }

    private T getDiffLog(String key, String keyDesc, Object obj, Object oldObj, Object newObj, FieldTypeEnum fieldTypeEnum, boolean isJson) {
        DiffType diffType = DiffType.UPDATE;
        if (newObj == null && oldObj != null) {
            diffType = DiffType.DELETE;
        }
        if (oldObj == null && newObj != null) {
            diffType = DiffType.ADD;
        }
        return onDiffSuccess.buildDiffLog(key, keyDesc, obj, oldObj, newObj, diffType, fieldTypeEnum, isJson);
    }

    public static void main(String[] args) throws Exception {
        @Data
        class TestObjItem {
            private String id;
            @DiffLog(name = "子编码", key = true)
            private String code;
            @DiffLog(name = "子名称")
            private String title;
            @DiffLog(name = "价格", scale = 2, roundingMode = BigDecimal.ROUND_HALF_UP)
            private BigDecimal price;
        }
        @Data
        class TestObj {
            @DiffLog(format = {"1=测试", "2=测试2"})
            private String id;
            @DiffLog(name = "编码", key = true)
            private String code;
            @DiffLog(name = "名称")
            private String title;
            @DiffLog(name = "明细")
            private List<TestObjItem> item;
        }

        TestObj testObj = new TestObj();
        testObj.setId("1");
        testObj.setCode("T1");
        testObj.setTitle("名称1");
        TestObjItem testObjItem = new TestObjItem();
        testObjItem.setId("20");
        testObjItem.setCode("TC1");
        testObjItem.setTitle("子名称1");
        testObjItem.setPrice(new BigDecimal("1.123"));
        TestObjItem testObjItem1 = new TestObjItem();
        testObjItem1.setId("21");
        testObjItem1.setCode("TC2");
        testObjItem1.setTitle("子名称2");
        testObjItem1.setPrice(new BigDecimal("2.123"));
        testObj.setItem(Arrays.asList(testObjItem, testObjItem1));
        TestObj testObj1 = new TestObj();
        testObj1.setId("2");
        testObj1.setCode("T2");
        testObj1.setTitle("名称2");
        TestObjItem testObjItem2 = new TestObjItem();
        testObjItem2.setId("21");
        testObjItem2.setCode("TC1");
        testObjItem2.setTitle("子名称111");
        testObjItem2.setPrice(new BigDecimal("9.523"));
        TestObjItem testObjItem3 = new TestObjItem();
        testObjItem3.setId("22");
        testObjItem3.setCode("TC3");
        testObjItem3.setTitle("子名称333");
        testObjItem3.setPrice(new BigDecimal("3.523"));
        testObj1.setItem(Arrays.asList(testObjItem2, testObjItem3));

        ObjectDiffUtil.onDiffSuccess((key, keyDesc, obj, oldObj, newObj, diffType, fieldTypeEnum, isJson) -> {
            System.out.println(key + ": " + keyDesc + " " + oldObj + " ===> " + newObj + " " + diffType + " - " + fieldTypeEnum);
            return new TestObj();
        }).diff(testObj, testObj1);
    }

}
